// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <inttypes.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#include <fbl/algorithm.h>
#include <fbl/alloc_checker.h>
#include <fbl/ref_ptr.h>
#include <fbl/unique_ptr.h>
#include <lib/fdio/vfs.h>
#include <fs/vfs.h>
#include <lib/memfs/cpp/vnode.h>
#include <zircon/device/vfs.h>

#include "dnode.h"

namespace memfs {

// Artificially cap the maximum in-memory file size to 512MB.
constexpr size_t kMemfsMaxFileSize = 512 * 1024 * 1024;

VnodeFile::VnodeFile(Vfs* vfs) : VnodeMemfs(vfs), vmo_size_(0), length_(0) {}

VnodeFile::~VnodeFile() { vfs()->WillFreeVMO(vmo_size_); }

zx_status_t VnodeFile::ValidateFlags(uint32_t flags) {
  if (flags & ZX_FS_FLAG_DIRECTORY) {
    return ZX_ERR_NOT_DIR;
  }
  return ZX_OK;
}

zx_status_t VnodeFile::Read(void* data, size_t len, size_t off, size_t* out_actual) {
  if ((off >= length_) || (!vmo_.is_valid())) {
    *out_actual = 0;
    return ZX_OK;
  } else if (len > length_ - off) {
    len = length_ - off;
  }

  zx_status_t status = vmo_.read(data, off, len);
  if (status == ZX_OK) {
    *out_actual = len;
  }
  return status;
}

zx_status_t VnodeFile::Write(const void* data, size_t len, size_t offset, size_t* out_actual) {
  zx_status_t status;
  if (offset > kMemfsMaxFileSize) {
    return ZX_ERR_FILE_BIG;
  }
  size_t newlen;
  if (add_overflow(offset, len, &newlen)) {
    return ZX_ERR_FILE_BIG;
  }
  newlen = newlen > kMemfsMaxFileSize ? kMemfsMaxFileSize : newlen;
  if ((status = vfs()->GrowVMO(vmo_, vmo_size_, newlen, &vmo_size_)) != ZX_OK) {
    return status;
  }
  // Accessing beyond the end of the file? Extend it.
  if (offset > length_) {
    // Zero-extending the tail of the file by writing to
    // an offset beyond the end of the file.
    ZeroTail(length_, offset);
  }
  size_t writelen = newlen - offset;
  if ((status = vmo_.write(data, offset, writelen)) != ZX_OK) {
    return status;
  }
  UpdateModified();
  *out_actual = writelen;

  if (newlen > length_) {
    length_ = newlen;
  }
  if (writelen < len) {
    // short write because we're beyond the end of the permissible length
    return ZX_ERR_FILE_BIG;
  }
  return ZX_OK;
}

zx_status_t VnodeFile::Append(const void* data, size_t len, size_t* out_end, size_t* out_actual) {
  zx_status_t status = Write(data, len, length_, out_actual);
  *out_end = length_;
  return status;
}

zx_status_t VnodeFile::GetVmo(int flags, zx_handle_t* out_vmo, size_t* out_size) {
  zx_status_t status;
  if (!vmo_.is_valid()) {
    // First access to the file? Allocate it.
    if ((status = zx::vmo::create(0, ZX_VMO_RESIZABLE, &vmo_)) != ZX_OK) {
      return status;
    }
  }

  // Let clients map and set the names of their VMOs.
  zx_rights_t rights = ZX_RIGHTS_BASIC | ZX_RIGHT_MAP | ZX_RIGHTS_PROPERTY;
  rights |= (flags & fuchsia_io_VMO_FLAG_READ) ? ZX_RIGHT_READ : 0;
  rights |= (flags & fuchsia_io_VMO_FLAG_WRITE) ? ZX_RIGHT_WRITE : 0;
  rights |= (flags & fuchsia_io_VMO_FLAG_EXEC) ? ZX_RIGHT_EXECUTE : 0;
  zx::vmo result;
  if (flags & fuchsia_io_VMO_FLAG_PRIVATE) {
    if ((status = vmo_.create_child(ZX_VMO_CHILD_COPY_ON_WRITE, 0, length_, &result)) != ZX_OK) {
      return status;
    }

    if ((status = result.replace(rights, &result)) != ZX_OK) {
      return status;
    }
    *out_vmo = result.release();
    *out_size = length_;
    return ZX_OK;
  }

  if ((status = vmo_.duplicate(rights, &result)) != ZX_OK) {
    return status;
  }
  *out_vmo = result.release();
  *out_size = length_;
  return ZX_OK;
}

zx_status_t VnodeFile::Getattr(vnattr_t* attr) {
  memset(attr, 0, sizeof(vnattr_t));
  attr->inode = ino_;
  attr->mode = V_TYPE_FILE | V_IRUSR | V_IWUSR | V_IRGRP | V_IROTH;
  attr->size = length_;
  attr->blksize = kMemfsBlksize;
  attr->blkcount = fbl::round_up(attr->size, kMemfsBlksize) / VNATTR_BLKSIZE;
  attr->nlink = link_count_;
  attr->create_time = create_time_;
  attr->modify_time = modify_time_;
  return ZX_OK;
}

zx_status_t VnodeFile::GetNodeInfo(uint32_t flags, fuchsia_io_NodeInfo* info) {
  info->tag = fuchsia_io_NodeInfoTag_file;
  return ZX_OK;
}

zx_status_t VnodeFile::Truncate(size_t len) {
  zx_status_t status;
  if (len > kMemfsMaxFileSize) {
    return ZX_ERR_INVALID_ARGS;
  }
  if ((status = vfs()->GrowVMO(vmo_, vmo_size_, len, &vmo_size_)) != ZX_OK) {
    return status;
  }
  if (len < length_) {
    // Shrink the logical file length.
    // Zeroing the tail here is optional, but it saves memory.
    ZeroTail(len, length_);
  } else if (len > length_) {
    // Extend the logical file length.
    ZeroTail(length_, len);
  }

  length_ = len;
  UpdateModified();
  return ZX_OK;
}

void VnodeFile::ZeroTail(size_t start, size_t end) {
  constexpr size_t kPageSize = static_cast<size_t>(PAGE_SIZE);
  if (start % kPageSize != 0) {
    char buf[kPageSize];
    size_t ppage_size = kPageSize - (start % kPageSize);
    memset(buf, 0, ppage_size);
    ZX_ASSERT(vmo_.write(buf, start, ppage_size) == ZX_OK);
  }
  end = fbl::min(fbl::round_up(end, kPageSize), vmo_size_);
  uint64_t decommit_offset = fbl::round_up(start, kPageSize);
  uint64_t decommit_length = end - decommit_offset;

  if (decommit_length > 0) {
    ZX_ASSERT(vmo_.op_range(ZX_VMO_OP_DECOMMIT, decommit_offset, decommit_length, nullptr, 0) ==
              ZX_OK);
  }
}

}  // namespace memfs
